/*
 * Copyright 2008 Wink Saville
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.saville.serdes;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.json.me.*;
import com.saville.debug.Log;

/*
 * Serialize primitive types to a byte array.
 * 
 * TODO: Add read/write float/double
 * TODO: What about read/write char?
 * TODO: What about arrays of char/short/int/float/doubles?
 * TODO: This serializes in low byte first (little endian), but I notice that
 * DataOutputStream/DataInputStream uses high byte first (big endian). Should
 * I change this to high byte first?
 */
public class SerdesJson {

    public SerdesJson() {
        init();
    }

    public byte[] data() {
        return json.toString().getBytes();
    }

    public SerdesJson init() {
        json = new JSONStringer();
        return this;
    }
    
    public SerdesJson init(String jsonData) {
        try {
            jt = new JSONTokener(jsonData);
        } catch (Exception e) {
            return null;
        }
        return this;
    }

    public SerdesJson init(byte[] jsonData) {
        return init(new String(jsonData));
    }

    public SerdesJson init(byte[] jsonData, int offset, int length) {
        byte b[] = new byte[length];
        System.arraycopy(jsonData, offset, b, 0, length);
        return init(new String(b));
    }

    public void startObject() throws JSONException {
        json.object();
    }
    
    public void endObject() throws JSONException {
        json.endObject();
    }
    
    public void startArray() throws JSONException {
        json.array();
    }
    
    public void endArray() throws JSONException {
        json.endArray();
    }
    
    public void writeKey(String keyString) throws JSONException {
        json.key(keyString);
    }
    
    public void writeBoolean(boolean v) throws JSONException {
        json.value(v);
    }

    public void writeByte(int v) throws JSONException {
        json.value(v);
    }

    public void writeShort(int v) throws JSONException {
        json.value(v);
    }

    public void writeInt(int v) throws JSONException {
        json.value(v);
    }

    public void writeLong(long v) throws JSONException {
        json.value(v);
    }

    public void writeBytes(byte b[]) throws JSONException {
        json.value(b);
    }

    public void writeString(String str) throws JSONException {
        json.value(str);
    }

    public char nextChar() throws JSONException {
        return jt.nextClean();
    }
    
    public char peekNextChar() throws JSONException {
        char c = jt.nextClean();
        jt.back();
        return c;
    }
    
    public void startArrayRead() throws JSONException {
        char c = nextChar();
        if (c != '[') {
            throw new JSONException("SerdesJson#startArrayRead expecting '['");
        }
    }
    
    public boolean isEndArrayRead() throws JSONException {
        return peekNextChar() == ']';
    }
    
    public void endArrayRead() throws JSONException {
        char c = nextChar();
        if (c != ']') {
            throw new JSONException("SerdesJson#endArrayRead expecting ']'");
        }
    }
    
    public boolean readBoolean() throws JSONException {
        boolean v = (Boolean)jt.nextValue();
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public byte readByte() throws JSONException {
        byte v = (Byte)jt.nextValue();
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public int readUnsignedByte() throws JSONException {
        int v = (Integer)jt.nextValue();
        v &= 0xff;
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public short readShort() throws JSONException {
        short v = (Short)jt.nextValue();
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public int readUnsignedShort() throws JSONException {
        int v = (Integer)jt.nextValue();
        v &= 0xffff;
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public int readInt() throws JSONException {
        int v = (Integer)jt.nextValue();
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public long readLong() throws JSONException {
        int i;
        long v;
        Object o = jt.nextValue();
        
        try {
            i = (Integer)o;
            v = (long)i;
        } catch (Exception e) {
            v = (Long)o;
        }
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public byte[] readBytes() throws JSONException {
//        boolean v = (Boolean)jt.nextValue();
//        char c = jt.nextClean();
//        if (c != ',')
//            jt.back();
//        return v;
        return null; // FIXME: !!!!
    }

    public String readString() throws JSONException {
        String v;
        try {
            v = (String)jt.nextValue();
        } catch (Exception e) {
            v = null;
        }
        char c = jt.nextClean();
        if (c != ',')
            jt.back();
        return v;
    }

    public void writeObject(Object obj) {
        try {
            if (obj == null) {
                json.value(null);
            } else {
                ((ISerdes) obj).serialize(this);
            }
        } catch (Exception e) {
            // TODO: What should this really do?
            Log.print("SerdesJson: writeObject Bug");
            Log.printStackTrace(e);
            throw new RuntimeException("SerdesJson#writeObject Exception");
        }
    }
    
    public Object readObject() {
        Object o = null;
        char c;
        String typeId;

        try {
            c = jt.nextClean();
            if (c == 'n') {
                jt.back();
                if (jt.nextValue() != JSONObject.NULL)
                    throw new RuntimeException("SerdesJson#readObject expecting 'null'");
                o = null;
            } else if (c == '{') {
                typeId = jt.nextValue().toString();
        
                c = jt.nextClean();
                if (c != ':') {
                    throw new RuntimeException("SerdesJson#readObject expecting ':' separator");
                }
                
                ISerdes s = mapping.get(typeId);
                if (s == null) {
                    /* TODO: Load the class here? */
                    throw new RuntimeException("Serdes: id=" + typeId + " not present");
                }
                s = (ISerdes) s.make();
                o = s.deserialize(this);
                
                if (jt.nextClean() != '}') {
                    throw new RuntimeException("SerdesJson#readObject must end with '}'");
                }
            } else {
                throw new RuntimeException("SerdesJson#readObject expecting '{'");
            }
        
            return o;
        } catch (JSONException e) {
            throw new RuntimeException("SerdesJson#readObject JSONException");
        }
    }

    /*
     * register an object that implements ISerdes
     * 
     * @throws RuntimeException if a mapping already exists (this is bug)
     */
    public static void register(int id, ISerdes s) {
        Object o = mapping.put(((Integer)id).toString(), s);
        if (o != null) {
            throw new RuntimeException("Serdes: Mapping for " + id + " already exists for " + o);
        }
    }

    public JSONStringer json;
    public JSONTokener jt;
    
    private static Map<String, ISerdes> mapping = Collections
            .synchronizedMap(new HashMap<String, ISerdes>());
}
